package org.swift.mashup.xslt;


import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import net.sf.saxon.event.Builder;
import net.sf.saxon.expr.XPathContext;
import net.sf.saxon.functions.EscapeURI;
import net.sf.saxon.om.Axis;
import net.sf.saxon.om.AxisIterator;
import net.sf.saxon.om.Item;
import net.sf.saxon.om.NodeInfo;
import net.sf.saxon.pattern.AnyNodeTest;
import net.sf.saxon.pattern.LocalNameTest;
import net.sf.saxon.pattern.NodeTest;
import net.sf.saxon.type.Type;

import com.fasterxml.uuid.Generators;
import com.fasterxml.uuid.impl.RandomBasedGenerator;
import org.apache.axiom.om.OMFactory;
import org.apache.log4j.Logger;

/**
 * Add xmlns:swift="java:org.swift.mashup.xslt.Extension" to mx:mashup to
 * make the functions of this class available to your mashup.
 */
public class Extension {
	private static final Logger log = Logger.getLogger(Extension.class);

	private static final String SPARQL_PARAM_ACCEPT = "Accept";
	private static final String SPARQL_PARAM_INFER = "infer";
	private static final String SPARQL_PARAM_QUERY = "query";
	private static final String SPARQL_PARAM_LANGUAGE = "queryLn";
	private static final String LANGUAGE_SPARQL = "SPARQL";
	private static final String ALPHABET = "abcdefghijklmnopqrstuvwxyz";
	private static final String HDF5_DELIM = "\" \"";

	public static String formEncode(XPathContext context, NodeInfo root) {
		return formEncode(context, root, null);
	}
	
	/**
	 * Converts an xml structure into a form-encoded string suitable for post-data.
	 * Expects to receive a single element with one layer of child nodes.  The 
	 * local names of the child nodes become the names of the form parameters, and
	 * the string values of the child nodes become the values of the form parameters.
	 * 
	 * If nameAttribute is provided, the child nodes will be searched for an attribute
	 * of the given name; if found, the value of that attribute will be used instead of
	 * the local-name of the node.
	 * 
	 * Add xmlns:swift="java:org.swift.mashup.xslt.Extension" to mx:mashup to
	 * make this function available to your mashup as swift:form-encode()
	 * 
	 * Example:
	 * <xsl:variable name="submission">
	 * 	<data>
	 * 		<firstname>Joey Joe Joe</firstname>
	 * 		<lastname>Shabadoo</lastname>
	 * 		<param key="url">http://www.rjlg.com/</param>
	 * 	</data>
	 * </xsl:variable>
	 * <xsl:value-of select="swift:form-encode($submission, 'key')" />
	 * 
	 * would output:
	 * firstname=Joey%20Joe%20Joe&lastname=Shabadoo&url=http%3A%2F%2Fwww%2Erjlg%2Ecom%2F
	 * 
	 * @param context
	 * @param root
	 * @param nameAttribute
	 * @return a string encoded in "application/x-www-form-urlencoded"
	 */
	public static String formEncode(XPathContext context, NodeInfo root, String nameAttribute) {
		StringBuilder sb = new StringBuilder();

		AxisIterator r = root.iterateAxis(Axis.CHILD, AnyNodeTest.getInstance());
		AxisIterator c = (root.getNodeKind() == Type.DOCUMENT) ? r.iterateAxis(Axis.CHILD, AnyNodeTest.getInstance()) : r ;
		NodeTest attributeTest = (nameAttribute != null) ? new LocalNameTest(context.getNamePool(), Type.ATTRIBUTE, nameAttribute) : null;
		
		while ( c.moveNext() ) {
			Item child = c.current();
			if ( child instanceof NodeInfo ) {
				NodeInfo childnode = (NodeInfo)child;
				
				if ( sb.length() > 0 ) {
					sb.append('&');
				}
				
				String name = childnode.getLocalPart();
				if ( attributeTest != null ) {
					AxisIterator a = childnode.iterateAxis(Axis.ATTRIBUTE, attributeTest);
					if ( a.moveNext() ) {
						name = ((NodeInfo)a.current()).getStringValue();
					}
				}
				
				sb.append(name);
				sb.append('=');
				sb.append(EscapeURI.escape(childnode.getStringValueCS(),""));
			}
		}

		String result = sb.toString();
		if ( log.isDebugEnabled() ) {
			log.debug("formEncode() - " + result);
		}
		return result;
	}
	
/*	public static String sparqlEncode(XPathContext context, String accept, NodeInfo queryNode, String... inputs) {
		return sparqlEncode(context, accept, queryNode.getStringValue(), inputs);
	}
*/	
	public static String sparqlEncode(XPathContext context, String infer, String accept, String query, String input) {
		return sparqlEncode(context, infer, accept, query, input, null);
	}
	
	public static String sparqlEncode(XPathContext context, String infer, String accept, String query, String input, String inputDelimiter) {
		StringBuffer sb = new StringBuffer();
		
		int varcount = sparqlCountVariables(query);
		Boolean bInfer = Boolean.parseBoolean(infer);
		
		List<String> inputs = sparqlTokenizeInputs(input, inputDelimiter);
		if ( inputs.size() != varcount ) {
			throw new RuntimeException(
				String.format("Query expects %d input variables, but %d arguments were provided.", varcount, inputs.size())
			);
		}
		
		sb.append( sparqlEncodePair(SPARQL_PARAM_ACCEPT, accept) );
		sb.append( '&' );
		sb.append( sparqlEncodePair(SPARQL_PARAM_INFER, bInfer.toString()) );
		sb.append( '&' );
		sb.append( sparqlEncodePair(SPARQL_PARAM_LANGUAGE, LANGUAGE_SPARQL) );
		sb.append( '&' );
		sb.append( sparqlEncodePair(SPARQL_PARAM_QUERY, sparqlBracketizeURIs(query)) );
		
		for ( int i = 0; i < inputs.size(); i++ ) {
			sb.append( '&' );
			sb.append( sparqlEncodePair("$"+ALPHABET.charAt(i), sparqlFormatValue(inputs.get(i))) );
		}
		
		String result = sb.toString();
		if ( log.isDebugEnabled() ) {
			log.debug("sparqlEncode() - " + result);
		}
		return result;
	}

	protected static List<String> sparqlTokenizeInputs(String inputs, String delimiter) {
		if ( delimiter == null ) {
			return Arrays.asList(inputs);
		}
		
		List<String> list = new ArrayList<String>();
		StringTokenizer tokenizer = new StringTokenizer(inputs, delimiter, false);
		while ( tokenizer.hasMoreTokens() ) {
			String tok = tokenizer.nextToken().trim();
			if ( !tok.isEmpty() ) {
				list.add( tok );
			}
		}
		
		return list;
	}

	protected static String sparqlEncodePair(String key, String value) {
		return key + '=' + EscapeURI.escape((CharSequence)value,"");
	}
	
	protected static String sparqlFormatValue(String value) {
		if ( value == null ) {
			return "";
		}
		if ( isUrl(value) ) {
			return "<"+value+">";
		}

		return '"'+value+'"';
	}
	
	protected static int sparqlCountVariables(String query) {
		Pattern pattern = Pattern.compile("(\\$([a-z])(\\s|$))");
		Matcher matcher = pattern.matcher(query);
		
		Set<String> vars = new HashSet<String>();
		while ( matcher.find() ) {
			vars.add( matcher.group(2) );
		}
		
		return vars.size();
	}
	
	protected static String sparqlBracketizeURIs(String str) {
		return str.replaceAll("(http(s)?://[^\\s>]+)", "<$1>")
				  .replaceAll("<<", "<")
				  .replaceAll(">>", ">");
	}
	
	public static boolean isUrl(String str) {
		if ( str == null ) {
			return false;
		}
		
		try {
			new URL(str);
			return true;
		}
		catch (MalformedURLException e) {
			return false;
		}
	}
	
	public static String generateUuid() {
		RandomBasedGenerator uuidGenerator = Generators.randomBasedGenerator();
		UUID uuid = uuidGenerator.generate();
		
		return uuid.toString();
	}


	public static List<String> tokenizeHdf5DataRows(String str, final int columnCount) {
		return tokenizeHdf5DataRows(str, columnCount, HDF5_DELIM);
	}

	/**
	 * Tokenize a delimited string from an HDF5 xml file into separate rows by counting the number of columns
	 * expected in each row.
	 *
	 * @param str   the string to tokenize
	 * @param columnCount   number of columns in each row
	 * @param delimiter string that delimits column values
	 * @return List&lt;String&gt; representing one string per row
	 *
	 * @see #tokenizeHdf5DataFields(String, String, boolean)
	 */
	public static List<String> tokenizeHdf5DataRows(String str, final int columnCount, final String delimiter) {
		final List<String> rows = new ArrayList<String>();

		if(columnCount > 0) {

			final StringBuilder sb = new StringBuilder();
			String[] parts = str.split(delimiter);

			int p = 0;
			while ( p < parts.length ) {
				final int partsRemaining = (parts.length - p);
				final int columns = partsRemaining > columnCount ? columnCount : partsRemaining;
				for ( int c = 0; c < columns; c++ ) {
					if ( c > 0 ) {
						sb.append(delimiter);
					}
					sb.append( parts[p++] );
				}

				rows.add(sb.toString());
				sb.setLength(0);
			}
		}

		return rows;
	}

	public static List<String> tokenizeHdf5DataFields(String str) {
		return tokenizeHdf5DataFields(str, HDF5_DELIM, true);
	}

	/**
	 * Given an HDF5-delimited string representing a single row of data, tokenize it into separate column fields.
	 *
	 * @param str   the string to tokenize
	 * @param delimiter string that delimits column values
	 * @return List&lt;String&gt; representing one string per field
	 *
	 * @see #tokenizeHdf5DataRows(String, int, String)
	 */
	public static List<String> tokenizeHdf5DataFields(String str, final String delimiter, final boolean removeQuotes) {
		List<String> fields = new ArrayList<String>();

		String[] parts = str.split(delimiter);

		for ( String part : parts ) {
			if ( removeQuotes ) {
				if ( part.startsWith("\"") ) {
					part = part.substring(1);
				}
				if ( part.endsWith("\"") ) {
					part = part.substring(0, part.length()-1);
				}
			}

			fields.add(part);
		}

		return fields;
	}
}
